
// Copyright (c) 2009, David Aiken
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the organization to which the authors belong nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY AUTHORS AND CONTRIBUTORS ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL AUTHORS AND CONTRIBUTORS BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

/*
 *----------------------------------------------------------------------------
 * The PRT SOP.  This SOP saves float32 data to the Krakatoa PRT format
 */
#define HDK_RELAXED_GB

// ignore deprecated warnings.. there are quite a few
#ifdef _MSC_VER
#pragma warning(disable:4996)
#else
#pragma GCC diagnostic ignored "-Wstrict-aliasing"
#endif

#include <UT/UT_DSOVersion.h>
#include <UT/UT_Math.h>
#include <UT/UT_Matrix3.h>
#include <UT/UT_Matrix4.h>
#include <UT/UT_Axis.h>
#include <GEO/GEO_AttributeHandle.h>
#include <GU/GU_Detail.h>
#include <GU/GU_PrimPoly.h>
#include <PRM/PRM_Include.h>
#include <OP/OP_Operator.h>
#include <OP/OP_OperatorTable.h>
#include <OP/OP_Director.h>
#include <SOP/SOP_Error.h>
#include <UT/UT_Interrupt.h>
#include <UT/UT_Args.h>
#include <string.h>
#include <string>
#include <ctype.h>

#include "ProjectConfig.h"
#include "SOP_prt.h"

#include "PRT.cpp"

using namespace HDK_PRT;

#include <UT/UT_Exit.h>

#define EPSILON 0.00001f // this is very fast/loose

inline int
Stricmp(const char *psz1, const char *psz2)
{
    register char c1, c2;
    do
    {
        c1 = tolower(*psz1++);
        c2 = tolower(*psz2++);
    } while ( c1 && (c1 == c2) );

    return c1 - c2;
}

//! Trim Both leading and trailing spaces
void trimSpaces( string& str)
{
    size_t startpos = str.find_first_not_of(" \t");
    size_t endpos = str.find_last_not_of(" \t");

    if(( string::npos == startpos ) || ( string::npos == endpos))
    {
        str = "";
    }
    else
    {
        str = str.substr( startpos, endpos-startpos+1 );
    }
}

void
newSopOperator(OP_OperatorTable *table)
{
    static bool initialized = false;

    if (!initialized)
    {
        initialized = true;
    }

    std::string sopName = "PRT 1.0";

    table->addOperator(
        new OP_Operator(
            "hdk_prt",                // internal name
            sopName.c_str(),            // UI name
            SOP_PRT::myConstructor,
            SOP_PRT::myTemplateList,
            0,                          // Must have 0 or more inputs
            1,                          // Max 1 inputs
            0)
    );
}

class TDataClosure
{
public:
    TDataClosure(){}
    virtual void moveAttribToCh()
    {
        throw SOP_PRT_Exception(exceptionError, "moveAttribToCh is not callable");
    }
    virtual void moveChToAttrib()
    {
        throw SOP_PRT_Exception(exceptionError, "readNextDataEntry is not callable");
    }
};

void
getTransformMatrix(UT_Matrix4 &mRet, bool computeInverse=false, bool isNormal=false)
{
    static bool initialized = false;
    static UT_Matrix4 m, mNormal;
    static UT_Matrix4 mInverse, mNormalInverse;

    if(!initialized)
    {
        float theta = M_PI/2.0f;
        UT_Matrix4 rotAboutX = UT_Matrix4::rotationMat(UT_Axis3::XAXIS, theta);
        UT_Matrix4 rotAboutY = UT_Matrix4::rotationMat(UT_Axis3::ZAXIS, theta);
        m = rotAboutX * rotAboutY;

        mNormal = m;
        mNormal.invert();
        mNormal.transpose();

        mInverse = m;
        mInverse.invert();

        mNormalInverse = mNormal;
        mNormalInverse.invert();

        initialized = true;
    }

    if(computeInverse)
    {
        mRet = (isNormal)?mNormalInverse:mInverse;
    }
    else
    {
        mRet = (isNormal)?mNormal:m;
    }
}

template<typename T>
class TStoreAttribDataClosure : public TDataClosure
{
private:
    GB_AttributeHandle *mH;
    T (*mFN)(GB_AttributeHandle *, int);
    int mArity;
    bool mConvertTo3DSCoordSpace;
    T *mIterator;
public:
    TStoreAttribDataClosure(GEO_AttributeHandle *ph, T (*fn)(GB_AttributeHandle *, int), int arity, bool convertTo3DSCoordSpace, void *pArray):
      mH(ph), mFN(fn), mArity(arity), mConvertTo3DSCoordSpace(convertTo3DSCoordSpace), mIterator((T*)pArray), TDataClosure()
    {
    }
    virtual void moveAttribToCh()
    {
        T *iterator = mIterator;
        for(int i=0;i<mArity;i++)
        {
            *mIterator = (*mFN)(mH, i);
            mIterator++;
        }

        if(mConvertTo3DSCoordSpace)
        {
            if(mArity == 3)
            {
                UT_Vector3 v(*iterator, *(iterator+1), *(iterator+2));
                GB_AttribType type;

                const GB_Attribute *attrib = mH->getAttribute();
                if(!attrib)
                {
                    // the "P" attribute is special. Houdini makes it look like an
                    // attribute but it isn't completely compatible.
                    const char* name = mH->getName();
                    if(0 == Stricmp(name, "P"))
                    {
                        type = GB_ATTRIB_FLOAT;
                    }
                    else
                    {
                        throw SOP_PRT_Exception(exceptionError, "getAttribute() failed.");
                    }
                }
                else
                {
                    type = attrib->getType();
                }

                UT_Matrix4 m;
                if(type == GB_ATTRIB_FLOAT)
                {
                    getTransformMatrix(m, false);
                }
                else if(type == GB_ATTRIB_VECTOR)
                {
                    getTransformMatrix(m, false, true);
                }
                else
                {
                    throw SOP_PRT_Exception(exceptionError, "can only convert attributes to 3DS Max space if they are of type GB_ATTRIB_FLOAT or GB_ATTRIB_VECTOR.");
                }
                v *= m;

                for(int i=0;i<mArity;i++)
                {
                    *iterator = v[i];
                    iterator++;
                }
            }
            else
            {
                throw SOP_PRT_Exception(exceptionError, "can only convert attributes to 3DS Max space if they have an arity of 3.");
            }
        }
    }
};

template<typename T>
T getDataF(GB_AttributeHandle *hAttr, int arity)
{
    T val = hAttr->getF(arity);
    return val;
}

template<typename T>
T getDataI(GB_AttributeHandle *hAttr, int arity)
{
    T val = hAttr->getI(arity);
    return val;
}

class CStoreDataClosure
{
private:
    TDataClosure *mClosure;
public:
    CStoreDataClosure(GEO_AttributeHandle &hAttr, DataType type, bool convertTo3DSCoordSpace, int arity, void *pArray):
      mClosure(NULL)
    {
        switch(type)
        {
            case kFLOAT16:
                mClosure = new TStoreAttribDataClosure<half>(&hAttr, getDataF<half>, arity, convertTo3DSCoordSpace, pArray);
                break;
            case kFLOAT32:
                mClosure = new TStoreAttribDataClosure<float>(&hAttr, getDataF<float>, arity, convertTo3DSCoordSpace, pArray);
                break;
            case kFLOAT64:
                mClosure = new TStoreAttribDataClosure<double>(&hAttr, getDataF<double>, arity, convertTo3DSCoordSpace, pArray);
                break;
            case kINT32:
                mClosure = new TStoreAttribDataClosure<int>(&hAttr, getDataI<int>, arity, convertTo3DSCoordSpace, pArray);
                break;
            case kINT64:
                mClosure = new TStoreAttribDataClosure<long long>(&hAttr, getDataI<long long>, arity, convertTo3DSCoordSpace, pArray);
                break;
            default:
                throw SOP_PRT_Exception(exceptionError, "attribute type is invalid");
        }
    }
    ~CStoreDataClosure()
    {
        if(mClosure)
        {
            delete(mClosure);
        }
    }
    void moveAttribToCh()
    {
        mClosure->moveAttribToCh();
    }
};

int
SOP_PRT::updateMode(void *data, int index, float time, const PRM_Template *tplate )
{
    SOP_PRT *me = (SOP_PRT *) data;
    me->disableParms();

    return 1;
}

static PRM_Default frameDefault(0, "$FF");

static PRM_Name names[] =
{
    // Setup parameters
    PRM_Name(MODE_PARM,            "Mode"),
    PRM_Name(FILE_NAME_PARM,       "File Name"),
    PRM_Name(RELOAD_GEOMETRY_PARM, "Reload Geometry"),
    PRM_Name(MISSING_FRAME_PARM,   "Missing Frame"),
    PRM_Name(ATTRS_PARM,           "Attributes"),

    PRM_Name(0)
};


static PRM_Name theModeMenu[] =
{
    PRM_Name("0", "Automatic"),
    PRM_Name("1", "Read"),
    PRM_Name("2", "Write"),
    PRM_Name("3", "No Operation"),
    PRM_Name(0),
};

static PRM_Name theMissingFrameMenu[] =
{
    PRM_Name("0", "Report Error"),
    PRM_Name("1", "No Geometry"),
    PRM_Name(0),
};

static PRM_Default nameDefault_filename(0,"$HIP/default$F.prt");
static PRM_Default nameDefault_attribs(0,"");

static PRM_ChoiceList modeMenu((PRM_ChoiceListType)(PRM_CHOICELIST_EXCLUSIVE | PRM_CHOICELIST_REPLACE), theModeMenu);
static PRM_ChoiceList missingFrameMenu((PRM_ChoiceListType)(PRM_CHOICELIST_EXCLUSIVE | PRM_CHOICELIST_REPLACE), theMissingFrameMenu);

static PRM_Name
storageTypeNames[] =
{
    PRM_Name("16bit", "16-bit"),
    PRM_Name("32bit", "32-bit"),
    PRM_Name("64bit", "64-bit"),
    PRM_Name(0)
};

static PRM_ChoiceList
storageTypeNamesMenu(PRM_CHOICELIST_SINGLE, storageTypeNames);
static PRM_Default storageTypeDefault(SOP_STORAGETYPE_32);

static PRM_Name
attribStorageMenu[] =
{
    PRM_Name("attrib#", "Attribute #"),
    PRM_Name("storagetype#", "Type #"),
    PRM_Name("convertTo3DSCoordSpace#", "Convert to 3DS Max coordinate space #"),
    PRM_Name(0)
};

static PRM_Template
attribTemplates[] =
{
    PRM_Template(PRM_STRING, 1, &attribStorageMenu[0], &nameDefault_attribs),
    PRM_Template(PRM_ORD, 1, &attribStorageMenu[1], &storageTypeDefault, &storageTypeNamesMenu),
    PRM_Template(PRM_TOGGLE, 1, &attribStorageMenu[2]),
    PRM_Template()
};

PRM_Template
SOP_PRT::myTemplateList[NUM_GUI_PARMS+2] =
{
    // Mode
    PRM_Template(PRM_ORD, 1, &names[0], PRMzeroDefaults, &modeMenu, 0, SOP_PRT::updateMode),

    // Filename of the file to be exported
    PRM_Template(PRM_FILE, 1, &names[1], &nameDefault_filename, 0),

    // Reload Geometry button
    PRM_Template(PRM_CALLBACK, 1, &names[2], 0, 0, 0, SOP_PRT::reloadGeometry),

    // How to handle missing frames
    PRM_Template(PRM_ORD, 1, &names[3], PRMzeroDefaults, &missingFrameMenu, 0),

    // Attributes to export
    PRM_Template(PRM_MULTITYPE_LIST, attribTemplates, 2, &names[4]),

    PRM_Template()
};

unsigned int
SOP_PRT::disableParms()
{
    unsigned int changed = 1; // something is always changed
    enumMode mode = MODE(CHgetEvalTime());
    for(int i=0; i <= NUM_GUI_PARMS; i++)
    {
        enableParm(i, false);
    }
    enableParm(MODE_PARM, true);

    switch(mode)
    {
        case modeAutomatic:
            {
                enableParm(FILE_NAME_PARM, true);
                if(nInputs() > 0)
                {
                    // write mode
                    enableParm(ATTRS_PARM, true);
                }
                else
                {
                    // read mode
                    enableParm(RELOAD_GEOMETRY_PARM, true);
                    enableParm(MISSING_FRAME_PARM, true);
                }
            }
            break;
        case modeRead:
            {
                enableParm(FILE_NAME_PARM, true);
                enableParm(RELOAD_GEOMETRY_PARM, true);
                enableParm(MISSING_FRAME_PARM, true);
            }
            break;
        case modeWrite:
            {
                enableParm(FILE_NAME_PARM, true);
                enableParm(ATTRS_PARM, true); // could support it at some point..
            }
            break;
        case modeNoOperation:
        default:
            {
            }
            break;
    }
    return changed;
}

OP_Node *
SOP_PRT::myConstructor(OP_Network *net, const char *name, OP_Operator *op)
{
    return new SOP_PRT(net, name, op);
}

SOP_PRT::SOP_PRT(OP_Network *net, const char *name, OP_Operator *op)
    : SOP_Node(net, name, op), mCalledFromCallback(false)
{
    #ifdef SOP_MAJOR_VER
    mSOPVersion = SOP_MAJOR_VER +  "." + SOP_MINOR_VER;
    #endif
}

SOP_PRT::~SOP_PRT()
{
}

//!Handles any issues when converting from Houdini attributes to PRT channels
const char *
convertToKrakatoaChannel(const char* houdiniName, DataType type, int &arity)
{
    if(arity < 1 || arity > 4)
    {
        throw SOP_PRT_Exception(exceptionError, "Unhandled arity: %u for channel name %s", arity, houdiniName);
    }

    if(0 == Stricmp(houdiniName, "P"))
    {
        // Krakatoa only uses the x/y/z coordinates
        arity = 3;
        return "Position";
    }

    if(0 == Stricmp(houdiniName, "v"))
    {
        return "Velocity";
    }
    if(0 == Stricmp(houdiniName, "Cd"))
    {
        return "Color";
    }

    // ensure the name meets PRT criteria
    for(unsigned int i=0;i<strlen(houdiniName);i++)
    {
        if(!(isalnum(houdiniName[i]) || houdiniName[i] == '_'))
        {
            throw SOP_PRT_Exception(exceptionError, "channel name %s contains invalid character: '%c'", houdiniName, houdiniName[i]);
        }
    }

    return houdiniName;
}

//!Handles any issues when converting from PRT channels to Houdini attributes
void
convertToHoudiniAttrib(SOPAttribute &ch, GB_AttribType &houdiniAttribType)
{
    const char *name = ch.mName.c_str();
    if((ch.mArity < 1) || (ch.mArity > 4))
    {
        throw SOP_PRT_Exception(exceptionError, "channel name %s has invalid arity: '%d'", name, ch.mArity);
    }

    if(0 == Stricmp(name, "Position"))
    {
        ch.mName = "P";
    }
    else if(0 == Stricmp(name, "Velocity"))
    {
        ch.mName = "v";
    }
    else if(0 == Stricmp(name, "Color"))
    {
        ch.mName = "Cd";
    }

    // N.B. we don't get enough info from PRT to create a
    //      GB_ATTRIB_VECTOR.
    switch(ch.mType)
    {
        case kFLOAT16:
        case kFLOAT32:
        case kFLOAT64:
        {
            houdiniAttribType = GB_ATTRIB_FLOAT;
            break;
        }
        case kINT32:
        case kINT64:
        {
            houdiniAttribType = GB_ATTRIB_INT;
            break;
        }
        default:
        {
            throw SOP_PRT_Exception(exceptionError, "channel name %s has invalid type: '%d'", name, ch.mType);
        }
    }
}

void
SOP_PRT::addChannel(unsigned int id, PRT& prt, const SOPAttribute &attr, long long pointCount)
{
    // at this point we convert Houdini P and v attributes
    // to Krakatoa Position and Velocity attributes.
    const char *name = attr.mName.c_str();

    // the validity of the attrib name has already been checked
    GEO_AttributeHandle hAttr = gdp->getPointAttribute(name);
    if(!hAttr.isAttributeValid())
    {
        throw SOP_PRT_Exception(exceptionError, "Invalid attribute: %s", name);
    }

    int arity = attr.mArity;

    // this will change the arity for the 'P' attribute
    const char* krakatoaChName = convertToKrakatoaChannel(name, attr.mType, arity);

    Array rawArray( new unsigned char[getDataLength(attr.mType)*arity*pointCount] );

    CStoreDataClosure closure(hAttr, attr.mType, attr.mConvertTo3DSCoordSpace, arity, rawArray.get());

    GEO_Point *pp;
    FOR_ALL_GPOINTS(gdp, pp)
    {
        hAttr.setElement(pp);

        // this iterates through the attribute data, storing it using
        // the DataType in rawArray
        closure.moveAttribToCh();
    }
    prt.addChannel(id, krakatoaChName, attr.mType, arity, rawArray);
}

void
SOP_PRT::writeFile(PRT &prt, OP_Context &context)
{
    float now = context.myTime;

    OP_ERROR err = lockInputs(context);
    if (err >= UT_ERROR_ABORT)
    {
        throw SOP_PRT_Exception(exceptionError, "Cannot lock inputs: %d", err);
    }

    duplicateSource(0, context);

    SOPAttribute attr;

    unsigned int attrCount = ATTR_COUNT();
    if(attrCount == 0)
    {
        throw SOP_PRT_Exception(exceptionError, "no attributes");
    }

    GEO_PointList pointlist = gdp->points();
    long long pointCount = pointlist.entries();

    bool pAttribSaved = false;
    for (unsigned int tok = 1; tok <= attrCount; tok++)
    {
        ATTR_NAME(attr.mName, tok, now);
        if(0 == attr.mName.length())
        {
            throw SOP_PRT_Exception(exceptionError, "empty attribute");
        }

        if(0 == Stricmp(attr.mName.c_str(), "P"))
        {
            pAttribSaved = true;
        }

        GEO_AttributeHandle h = gdp->getPointAttribute(attr.mName.c_str());
        if(!h.isAttributeValid())
        {
            throw SOP_PRT_Exception(exceptionError, "invalid attribute: '%s'", attr.mName.c_str());
        }
        bool isFloat = (h.getFloatCount(true) > 0);

        attr.mArity = h.entries();

        SOP_StorageType type = ATTR_TYPE(tok, now);
        switch(type)
        {
            case SOP_STORAGETYPE_16:
                if(!isFloat)
                {
                    throw SOP_PRT_Exception(exceptionError, "16-bit is not supported for integer attributes: '%s'", attr.mName.c_str());
                }
                attr.mType = kFLOAT16;
                break;
            case SOP_STORAGETYPE_32:
                attr.mType = (isFloat)?kFLOAT32:kINT32;
                break;
            case SOP_STORAGETYPE_64:
                attr.mType = (isFloat)?kFLOAT64:kINT64;
                break;
            default:
                throw SOP_PRT_Exception(exceptionError, "invalid data type: %d for attribute: '%s'", attr.mType, attr.mName.c_str());
                break;
        }
        attr.mConvertTo3DSCoordSpace = ATTR_CONVERT(tok, now);

        addChannel(tok, prt, attr, pointCount);
    }

    if(!pAttribSaved)
    {
        throw SOP_PRT_Exception(exceptionError, "you must include the 'P' attribute with arity 3");
    }

    UT_String fileName;
    FNAME(fileName, now);

    prt.saveToFile(pointCount, fileName.toStdString().c_str());
    unlockInputs();
}

inline size_t
getDataLength(const GB_AttribType& t)
{
    return 4; // for now Houdini supports 32-bit int or float attribs
}

void
SOP_PRT::addOrCreateAttrib(const string &attribName, GB_AttribType type, unsigned int arity)
{
    GEO_AttributeHandle h = gdp->getPointAttribute(attribName.c_str());
    if(!h.isAttributeValid())
    {
        // N.B. PRT does not have a type for GB_ATTRIB_VECTOR. If the
        //      user wants an attribute of this type it should be
        //      added to the point rather than relying on this SOP
        //      to add it here.

        GB_AttributeRef r = gdp->addPointAttrib(UT_String(attribName), getDataLength(type)*arity, type, NULL);
        if(!r.isValid())
        {
            throw SOP_PRT_Exception(exceptionError, "Unable to create attribute '%s'", attribName.c_str());
        }
    }
    else if((0 != Stricmp(attribName.c_str(), "P")) && h.entries() != arity)
    {
        throw SOP_PRT_Exception(exceptionError, "Existing attribute '%s' has arity %u but file has arity %u",
          attribName.c_str(), h.entries(), arity);
    }
}

//!Lower-level work class to do the actual data movement.
template<typename T>
class TReadAttribDataClosure : public TDataClosure
{
private:
    GB_AttributeHandle *mH;
    void (*mFN)(GB_AttributeHandle *, T, int);
    int mChArity;
    int mAttribArity;
    T *mIterator;
public:
    TReadAttribDataClosure(
        GEO_AttributeHandle *ph, void (*fn)(GB_AttributeHandle *, T, int),
        int chArity, int attribArity, void *pArray):
      mH(ph), mFN(fn), mChArity(chArity), mAttribArity(attribArity),
      mIterator((T*)pArray), TDataClosure()
    {
    }

    virtual void moveChToAttrib()
    {
        for(int i=0;i<mChArity;i++)
        {
            (*mFN)(mH, *mIterator, i);
            mIterator++;
        }

        // 'P' attrib has 4 coordinates but is stored in the 'Position' channel with only 3 so add '0' as the w entry
        for(int i=mChArity;i<mAttribArity;i++)
        {
            (*mFN)(mH, 0, i);
            mIterator++;
        }
    }
};

//!Sets Houdini float attribute
template<typename T>
void setDataF(GB_AttributeHandle *hAttr, T val, int index)
{
    hAttr->setF(float(val), index);
}

//!Sets Houdini integer attribute
template<typename T>
void setDataI(GB_AttributeHandle *hAttr, T val, int index)
{
    hAttr->setI(int(val), index);
}

//!Reads PRT data and stores it to the given Houdini attribute.
//!Houdini only understands float32 and int32 so there is some
//!rather clumsy shuffling of data types. This class allows
//!use of RAII to simplify resource management.
class CReadDataClosure
{
private:
    TDataClosure *mClosure;
public:
    CReadDataClosure(GEO_AttributeHandle &hAttr, DataType type, int chArity, int attribArity, void *pArray, int pnt):
      mClosure(NULL)
    {
        int i = pnt*chArity;
        switch(type)
        {
            case kFLOAT16:
                mClosure = new TReadAttribDataClosure<half>(&hAttr, setDataF<half>, chArity, attribArity, (unsigned char*)pArray+sizeof(half)*i);
                break;
            case kFLOAT32:
                mClosure = new TReadAttribDataClosure<float>(&hAttr, setDataF<float>, chArity, attribArity, (unsigned char*)pArray+sizeof(float)*i);
                break;
            case kFLOAT64:
                mClosure = new TReadAttribDataClosure<double>(&hAttr, setDataF<double>, chArity, attribArity, (unsigned char*)pArray+sizeof(double)*i);
                break;
            case kINT32:
                mClosure = new TReadAttribDataClosure<int>(&hAttr, setDataI<int>, chArity, attribArity, (unsigned char*)pArray+sizeof(int)*i);
                break;
            case kINT64:
                mClosure = new TReadAttribDataClosure<long long>(&hAttr, setDataI<long long>, chArity, attribArity, (unsigned char*)pArray+sizeof(long long)*i);
                break;
            default:
                throw SOP_PRT_Exception(exceptionError, "attribute type is invalid");
        }
    }

    ~CReadDataClosure()
    {
        if(mClosure)
        {
            delete(mClosure);
        }
    }

    //!Move PRT channel data to Houdini attribute
    void moveChToAttrib()
    {
        mClosure->moveChToAttrib();
    }
};

void
SOP_PRT::readFile(PRT &prt, OP_Context &context)
{
    float now = context.myTime;

    UT_String fileName;
    FNAME(fileName, now);

    // clean out all geometry
    gdp->clearAndDestroy();

    prt.readFromFile(fileName.toStdString().c_str());

    long long pointCount = prt.getCount();

    // register the point attributes
    PRTChannelDict dict;
    prt.getChannels(dict);
    PRTChannelDict::iterator dictIter;
    vector<SOPAttribute> SOPAttributes;
    for (dictIter=dict.begin(); dictIter != dict.end(); dictIter++)
    {
        SOPAttribute ch(&((*dictIter).second));
        GB_AttribType houdiniAttribType;
        convertToHoudiniAttrib(ch, houdiniAttribType);
        addOrCreateAttrib(ch.mName, houdiniAttribType, ch.mArity);
        SOPAttributes.push_back(ch);
    }

    // read in the points
    vector<SOPAttribute>::iterator i;
    for(long long pnt=0;pnt<pointCount;pnt++)
    {
        GEO_Point *p = gdp->appendPoint();
        if(!p)
        {
            throw SOP_PRT_Exception(exceptionError, "Unable to append point");
        }

        for (dictIter=dict.begin(), i=SOPAttributes.begin(); dictIter != dict.end(); dictIter++, i++)
        {
            SOPAttribute &ch = *i;
            GEO_AttributeHandle hAttr = gdp->getPointAttribute(ch.mName.c_str());
            if(!hAttr.isAttributeValid())
            {
                throw SOP_PRT_Exception(exceptionError, "Unable to get handle for attribute %s", ch.mName.c_str());
            }
            hAttr.setElement(p);

            CReadDataClosure closure(hAttr, ch.mType, ch.mArity, hAttr.entries(), ch.mData.get(), pnt);
            closure.moveChToAttrib();
        }
    }
}

//!Main entry point from Houdini
OP_ERROR
SOP_PRT::cookMySop(OP_Context &context)
{
    float now = context.myTime;

    enumMode mode;

    if(mCalledFromCallback)
    {
        mode = modeRead; // reload geometry requested
        mCalledFromCallback = false;
    }
    else
    {
        mode = MODE(now);
    }

    if(mode == modeNoOperation)
    {
        return error();
    }

    // Evaluate the GUI parameters
    UT_String fileName;
    FNAME(fileName, now);

    bool fileExists = false;
    FILE *f = fopen(fileName.toStdString().c_str(), "r");
    if(f)
    {
        fclose(f);
        fileExists = true;
    }

    if(mode == modeAutomatic)
    {
        // if no inputs, must read.
        // if inputs, read if file already exists otherwise write
        if(nInputs() == 0)
        {
            mode = modeRead;
        }
        else
        {
            if(fileExists)
            {
                mode = modeRead;
            }
            else
            {
                mode = modeWrite;
            }
        }
    }

    // Start the interrupt server
    UT_Interrupt *boss;
    boss = UTgetInterrupt();
    boss->opStart("Exporting PRT File");

    PRT prt;
    try
    {
        if((mode == modeWrite) && nInputs() == 0)
        {
            throw SOP_PRT_Exception(exceptionError, "Cannot write if no inputs");
        }

        if(mode == modeRead && !fileExists)
        {
            enumMissingFrame missingFrame = MISSING_FRAME(now);
            if(missingFrame == missingFrameReportError)
            {
                throw SOP_PRT_Exception(exceptionError, "Missing file: %s", fileName.toStdString().c_str());
            }
            SOP_PRT_Exception e(exceptionWarning, "Missing file: %s", fileName.toStdString().c_str());
            e.output();
        }

        disableParms();

        if (boss->opInterrupt(0.0))
        {
            throw SOP_PRT_Exception(exceptionWarning, "cook interrupted");
        }

        // Check to see that there hasn't been a critical error in cooking the SOP.
        if (error() >= UT_ERROR_ABORT)
        {
            throw SOP_PRT_Exception(exceptionError, "Unknown error: %d", error());
        }

        switch(mode)
        {
            case modeRead:
                readFile(prt, context);
                break;
            case modeWrite:
                writeFile(prt, context);
                break;
            case modeAutomatic:
            default:
                throw SOP_PRT_Exception(exceptionError, "invalid mode");
                break;
        }
    }
    catch (SOP_PRT_Exception &e)
    {
        UT_String m = e.getMsg();
        enumExceptionSeverity sev = e.getSeverity();
        if(sev == exceptionWarning)
        {
            addWarning(SOP_MESSAGE, m);
        }
        else
        {
            addError(SOP_MESSAGE, m);
        }
    }
    catch(std::runtime_error &e)
    {
        addError(SOP_MESSAGE, e.what());
    }
    catch(...)
    {
        addError(SOP_MESSAGE, "Unexpected exception");
    }
    boss->opEnd();

    return error();
}

//!Reload geometry. Should only be active in read mode.
int
SOP_PRT::reloadGeometry(void *data, int index, float time, const PRM_Template *tplate)
{
    bool retval = 1;

    // Set state to indicate that we are being called via pushbutton
    SOP_PRT *me = (SOP_PRT *) data;
    me->mCalledFromCallback = true;

    // Enter the main cook routine
    OP_Context myContext(time);
    myContext.setData((OP_ContextData*)data);
    me->cookMe(myContext);

    return retval;
}
